﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;

namespace Microsoft.Tools.WindowsInstallerXml.Extensions
{
    internal static class Extensions
    {
        #region Fields

        private const string StringFormatFallbackPattern = @"\{(?<index>[\d]+)[\:]?[\s\w\.\,\%\+\(\)#]*\}";

        #endregion

        #region Extend: System.String

        /// <summary>
        /// Formats the current instance.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string Format(this string instance, IFormatProvider provider, object arg0)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            try
            {
                return string.Format(provider, instance, arg0);
            }
            catch (FormatException)
            {
                return instance.Format(provider, new[] { arg0 });
            }
        }

        /// <summary>
        /// Formats the current instance.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <param name="arg1">The second argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string Format(this string instance, IFormatProvider provider, object arg0, object arg1)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            try
            {
                return string.Format(provider, instance, arg0, arg1);
            }
            catch (FormatException)
            {
                return instance.Format(provider, new[] { arg0, arg1 });
            }
        }

        /// <summary>
        /// Formats the current instance.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <param name="arg1">The second argument to merge.</param>
        /// <param name="arg2">The third argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string Format(this string instance, IFormatProvider provider, object arg0, object arg1,
                                    object arg2)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            try
            {
                return string.Format(provider, instance, arg0, arg1, arg2);
            }
            catch (FormatException)
            {
                return instance.Format(provider, new[] { arg0, arg1, arg2 });
            }
        }

        /// <summary>
        /// Formats the current instance.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <param name="args">A set of arguments to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string Format(this string instance, IFormatProvider provider, params object[] args)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            try
            {
                return string.Format(provider, instance, args);
            }
            catch (FormatException)
            {
                return Regex.Replace(instance, StringFormatFallbackPattern, delegate(Match match)
                                                                            {
                                                                                int index;

                                                                                return int.TryParse(
                                                                                    match.Groups["index"].Value,
                                                                                    out index) && index >= 0 &&
                                                                                       index < args.Length
                                                                                           ? match.Value.Format(
                                                                                               provider,
                                                                                               args)
                                                                                           : match.Value;
                                                                            });
            }
        }

        /// <summary>
        /// Formats the current instance based on the invariant culture's format provider.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string FormatInvariant(this string instance, object arg0)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            return instance.Format(CultureInfo.InvariantCulture, arg0);
        }

        /// <summary>
        /// Formats the current instance based on the invariant culture's format provider.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <param name="arg1">The second argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string FormatInvariant(this string instance, object arg0, object arg1)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            return instance.Format(CultureInfo.InvariantCulture, arg0, arg1);
        }

        /// <summary>
        /// Formats the current instance based on the invariant culture's format provider.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="arg0">The first argument to merge.</param>
        /// <param name="arg1">The second argument to merge.</param>
        /// <param name="arg2">The third argument to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string FormatInvariant(this string instance, object arg0, object arg1, object arg2)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            return instance.Format(CultureInfo.InvariantCulture, arg0, arg1, arg2);
        }

        /// <summary>
        /// Formats the current instance based on the invariant culture's format provider.
        /// </summary>
        /// <param name="instance">The target instance.</param>
        /// <param name="args">A set of arguments to merge.</param>
        /// <returns>The result of the format operation.</returns>
        public static string FormatInvariant(this string instance, params object[] args)
        {
            if (string.IsNullOrEmpty(instance)) return instance;

            return instance.Format(CultureInfo.InvariantCulture, args);
        }

        #endregion

        #region Extend: System.Xml.Linq.XDocument

        public static XmlNamespaceManager GetNamespaceManager(this XDocument document)
        {
            return document.GetNamespaceManager(null);
        }

        public static XmlNamespaceManager GetNamespaceManager(this XDocument document, string defaultNamespacePrefix)
        {
            using (var reader = document.CreateReader())
            {
                while (reader.Read())
                {
                    /* do nothing */
                }

                var nsMgr = new XmlNamespaceManager(reader.NameTable);

                if (!string.IsNullOrEmpty(defaultNamespacePrefix) && !nsMgr.HasNamespace(defaultNamespacePrefix))
                {
                    var ns = document.Root.GetDefaultNamespace();

                    if (ns == XNamespace.None)
                    {
                        ns = document.Root.Name.Namespace;
                    }

                    if (ns != XNamespace.None)
                    {
                        nsMgr.AddNamespace(defaultNamespacePrefix, ns.NamespaceName);
                    }
                }

                return nsMgr;
            }
        }

        #endregion

        #region Extend: System.Xml.Linq.XElement

        public static string AttributeValue(this XElement element, XName name)
        {
            XAttribute attribute = element.Attribute(name);

            return (attribute == null ? null : attribute.Value);
        }

        public static string GetAbsoluteXPath(this XElement element)
        {
            return element.GetAbsoluteXPath(null);
        }

        public static string GetAbsoluteXPath(this XElement element, string defaultNamespacePrefix)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }

            Func<XElement, string> relativeXPath = e =>
                                                   {
                                                       var index = e.IndexPosition();

                                                       string name;

                                                       if (e.Name.Namespace == null)
                                                       {
                                                           name = e.Name.LocalName;
                                                       }
                                                       else
                                                       {
                                                           string namespacePrefix =
                                                               e.GetPrefixOfNamespace(e.Name.Namespace);

                                                           if (string.IsNullOrEmpty(namespacePrefix))
                                                           {
                                                               if (string.IsNullOrEmpty(defaultNamespacePrefix))
                                                               {
                                                                   throw new NotSupportedException(
                                                                       "Missing default namespace prefix.");
                                                               }

                                                               namespacePrefix = defaultNamespacePrefix;
                                                           }

                                                           name = namespacePrefix + ":" + e.Name.LocalName;
                                                       }

                                                       return (index == -1)
                                                                  ? "/" + name
                                                                  : "/{0}[{1}]".FormatInvariant(name, index);
                                                   };

            IEnumerable<string> ancestors = from e in element.Ancestors()
                                            select relativeXPath(e);

            return string.Concat(ancestors.Reverse().ToArray()) + relativeXPath(element);
        }

        /// <summary>
        /// Get the index of the given XElement relative to its
        /// siblings with identical names. If the given element is
        /// the root, -1 is returned.
        /// </summary>
        /// <param name="element">
        /// The element to get the index of.
        /// </param>
        public static int IndexPosition(this XElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }

            if (element.Parent == null)
            {
                return -1;
            }

            var i = 1; // Indexes for nodes start at 1, not 0

            foreach (var sibling in element.Parent.Elements(element.Name))
            {
                if (sibling == element)
                {
                    return i;
                }

                i++;
            }

            throw new InvalidOperationException
                ("element has been removed from its parent.");
        }

        public static string GetInnerText(this XElement element)
        {
            var innerText = new StringBuilder();

            foreach (var node in element.Nodes().Where(node => node.NodeType == XmlNodeType.Text))
            {
                innerText.Append(node.ToString());
            }

            return innerText.ToString();
        }

        #endregion
    }
}